// --------------------------------------------------------------------------------------------------------------------
// <copyright file="AgentLocator.cs" company="Microsoft Corporation">
// The MIT License (MIT)
// 
// Copyright (c) 2014, Microsoft Corporation
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
namespace Microsoft.Robotics.Runtime
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Reflection;
    using System.Runtime.Serialization;
    using System.Xml;
    using Microsoft.Robotics.IO;
    using Microsoft.Robotics.PerformanceCounters;

    /// <summary>
    /// Creates and/or registers a group of agents that cooperate in some fashion. 
    /// The agent locator is only used at startup, to find and wire agents together. 
    /// Once the agents are wired and start processing messages, the locator is not used anymore.
    /// </summary>
    public sealed class AgentLocator
    {
        /// <summary>
        /// The mapping of agents to agent hosts that wrap them.
        /// </summary>
        private Dictionary<IAgent, AgentHost> agentsAndHosts;

        /// <summary>
        /// The mapping of agent names to agent hosts.
        /// </summary>
        private Dictionary<string, PipelineElement> namesAndHosts;

        /// <summary>
        /// The currently loaded set of manifests
        /// </summary>
        private List<Manifest> manifests;

        /// <summary>
        /// Synchronization object to prevent concurrent access to the two dictionaries above.
        /// </summary>
        private object syncRoot;

        /// <summary>
        /// Indicates whether to create the pipeline in online (default) or offline mode.
        /// See constructor below for more details.
        /// </summary>
        private bool offlineMode;

        /// <summary>
        /// The time when the locator was created. This is used as the time origin for agent message timestamps.
        /// </summary>
        private long creationTime;

        /// <summary>
        /// Initializes a new instance of the AgentLocator class.
        /// </summary>
        public AgentLocator()
            : this(false)
        {
        }

        /// <summary>
        /// Initializes a new instance of the AgentLocator class.
        /// </summary>
        /// <param name="offlineMode">
        /// Indicates whether to create the pipeline in online (default) or offline mode.
        /// If offlineMode is false, the pipeline is in online mode and the messages are exchanged in real time. 
        /// In this mode consumers always process the newest message. If a consumer cannot keep up with the incoming messages, some of the messages get dropped. 
        /// If offlineMode is true, the pipeline is created in offline mode, where no messages are dropped. 
        /// In this mode consumers always process the oldest message. If a consumer cannot keep up in real time, the entire pipeline is slowed down.
        /// </param>
        public AgentLocator(bool offlineMode)
        {
            this.agentsAndHosts = new Dictionary<IAgent, AgentHost>();
            this.namesAndHosts = new Dictionary<string, PipelineElement>();
            this.syncRoot = new object();
            this.manifests = new List<Manifest>();
            this.offlineMode = offlineMode;
            this.creationTime = AgentMessage.GetCurrentTime();
            PerformanceCounterManager.SetupCounters(false);
        }

        /// <summary>
        /// Gets the time origin for all agents in this pipeline.
        /// </summary>
        public long CreationTime
        {
            get { return this.creationTime; }
        }

        /// <summary>
        /// Gets an instance of an agent with the specified name. 
        /// Contract compatibility is validated later, when subscribing or requesting a control port.
        /// </summary>
        /// <param name="agentName">Agent name.</param>
        /// <returns>A reference to the agent.</returns>
        public IAgentHost GetAgent(string agentName)
        {
            lock (this.syncRoot)
            {
                if (!this.namesAndHosts.ContainsKey(agentName))
                {
                    throw new InvalidOperationException(
                        string.Format("An agent with name {0} could not be found.", agentName));
                }

                return this.namesAndHosts[agentName];
            }
        }

        /// <summary>
        /// Gets an instance of an agent with the specified name. 
        /// Contract compatibility is validated later, when subscribing or requesting a control port.
        /// </summary>
        /// <param name="agentName">Agent name.</param>
        /// <param name="host">The host associated with the name, if one is found.</param>
        /// <returns>A reference to the agent, or null if an agent with this name is not in the pipeline.</returns>
        public bool TryGetAgent(string agentName, out IAgentHost host)
        {
            lock (this.syncRoot)
            {
                if (!this.namesAndHosts.ContainsKey(agentName))
                {
                    host = null;
                    return false;
                }

                host = this.namesAndHosts[agentName];
            }

            return true;
        }

        /// <summary>
        /// Creates a synchronized subscription by injecting a synchronization agent in front of the subscriber passed in.
        /// </summary>
        /// <typeparam name="T1">The type of first message to synchronize</typeparam>
        /// <typeparam name="T2">The type of the second message to synchronize</typeparam>
        /// <param name="agent1">The name of the first agent to subscribe to</param>
        /// <param name="agent2">The name of the second agent to subscribe to</param>
        /// <param name="subscriber">The agent that requests this subscription</param>
        /// <param name="syncTolerance">The maximum difference between originating times, in milliseconds, above which two messages are considered unrelated</param>
        /// <param name="allowOverflow">
        /// Whether to allow either message queue to dump old messages that are still pending. 
        /// Should only be used when the frequency ratio between the two message types exceeds the queue size (1000)
        /// </param>
        /// <param name="disableCascadingActivation">If true, the sync agent doesn't activate/deactivate its producers. Useful for model agents</param>
        /// <returns>A synchronization agent that can be activated/deactivated on demand</returns>
        public IAgentHost JoinSubscribe<T1, T2>(string agent1, string agent2, ISubscriptionReceiver<MessageTuple<T1, T2>> subscriber, int syncTolerance, bool allowOverflow, bool disableCascadingActivation = false)
            where T1 : AgentMessage
            where T2 : AgentMessage
        {
            // create the join agent and add it to the agent pipeline, then subscribe to it.
            string syncName = string.Format("{0}(Sync)", ((IAgent)subscriber).Name);
            SynchronizationAgent<T1, T2> syncAgent = new SynchronizationAgent<T1, T2>(syncName, agent1, agent2, syncTolerance, allowOverflow, disableCascadingActivation);
            IAgentHost syncHost = this.Register(syncAgent);
            syncHost.Subscribe(subscriber);
            return syncHost;
        }

        /// <summary>
        /// Loads and wires agents from a manifest.
        /// The order of agents in the configuration file is important. Upstream agents must appear first, otherwise downstream agents cannot be wired.
        /// </summary>
        /// <param name="fileName">The full path of the file to load</param>
        public void LoadFrom(string fileName)
        {
            Manifest m = Serializer.Load<Manifest>(fileName);
            this.LoadFrom(m);
        }

        /// <summary>
        /// Loads and wires agents from a manifest.
        /// The order of agents in the configuration file is important. Upstream agents must appear first, otherwise downstream agents cannot be wired.
        /// </summary>
        /// <param name="manifest">The manifest to load</param>
        public void LoadFrom(Manifest manifest)
        {
            this.manifests.Add(manifest);

            // register the remote proxies
            foreach (ProxyDefinition proxyDefinition in manifest.RemoteAgentProxies)
            {
                UdpFarProxy proxy = new UdpFarProxy(proxyDefinition, this);
                this.RegisterProxy(proxy);
            }

            // register the local agents
            // create the hosts for each agent. The hosts in turn will extract additional information from each agent.
            foreach (IAgent agent in manifest.Agents)
            {
                this.Register(agent);
            }

            // register the list of agents to make visible to remote nodes
            foreach (ProxyDefinition proxyDefinition in manifest.NetworkEnabledAgentProxies)
            {
                UdpNearProxy proxy = new UdpNearProxy(proxyDefinition, this);
                this.RegisterProxy(proxy);
            }

            // activate the ones that need activation
            foreach (string agentName in manifest.ActiveAgents)
            {
                this.GetAgent(agentName).Activate();
            }
        }

        /// <summary>
        /// Deactivates all agents that have been previously activated because the manifest indicates so
        /// </summary>
        public void Deactivate()
        {
            foreach (Manifest m in this.manifests)
            {
                foreach (string name in m.ActiveAgents)
                {
                    IAgentHost agent = this.GetAgent(name);
                    if (agent.IsActive)
                    {
                        agent.Deactivate();
                    }
                }
            }
        }

        /// <summary>
        /// Registers an agent with the locator and initializes the agent to allow it to wire itself to the agents already registered.
        /// The order of registering agents maters, as the agent being registered can only see the agents already registered.
        /// </summary>
        /// <param name="agent">The agent to register.</param>
        /// <returns>The host that wraps the agent.</returns>
        internal IAgentHost Register(IAgent agent)
        {
            lock (this.syncRoot)
            {
                if (this.agentsAndHosts.ContainsKey(agent))
                {
                    return this.agentsAndHosts[agent];
                }

                if (this.namesAndHosts.ContainsKey(agent.Name))
                {
                    throw new InvalidOperationException(
                        string.Format(
                            "An agent with name {0} is already registered. Provide a unique name instead.", agent.Name));
                }

                AgentHost host = new AgentHost(agent, this);
                host.InitializePorts(this.offlineMode, this.creationTime);
                this.agentsAndHosts[agent] = host;
                this.namesAndHosts[agent.Name] = host;

                // initialize the agent
                // we are assuming the agents are provided in order, so that an agent that depends on another agent comes after it.
                // TODO: removing the ordering assumption is fairly easy to do (move the call to Initialize outside of here), and can be done later as needed.
                agent.Initialize(this);

                return host;
            }
        }

        /// <summary>
        /// Registers a proxy with the locator.
        /// </summary>
        /// <param name="proxyHost">the proxy to register. Non-proxy agent hosts should be registered with Register() instead.</param>
        internal void RegisterProxy(PipelineElement proxyHost)
        {
            lock (this.syncRoot)
            {
                if (this.namesAndHosts.ContainsKey(proxyHost.Name))
                {
                    throw new InvalidOperationException(
                        string.Format(
                            "An agent with name {0} is already registered. Provide a unique name instead.", 
                            proxyHost.Name));
                }

                proxyHost.InitializePorts(this.offlineMode, this.creationTime);
                this.namesAndHosts[proxyHost.Name] = proxyHost;
            }
        }

        /// <summary>
        /// Retrieves the agent host given an agent instance. 
        /// This is used by the publisher agent host when an agent is subscribing to it, to find the host of the subscriber.
        /// </summary>
        /// <param name="agent">The agent to get the host for.</param>
        /// <returns>The agent host wrapping the given agent.</returns>
        internal PipelineElement GetAgentHost(IAgent agent)
        {
            lock (this.syncRoot)
            {
                return this.agentsAndHosts[agent];
            }
        }
    }
}
